package com.fanvil.videoconference;

import javax.microedition.khronos.egl.EGL10;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.view.Surface;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import com.fanvil.videoconference.FvlLogger;

import android.graphics.SurfaceTexture;
import android.os.Build.VERSION;
import android.util.Log;

import java.util.HashMap;

public final class FvlRenderTarget {
    private static FvlLogger sLogger = FvlLogger.getLogger(FvlRenderTarget.class.getSimpleName(), FvlLogger.DEBUG);

    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final int EGL_OPENGL_ES2_BIT = 4;

    private static final int EGL_RECORDABLE_ANDROID = 0x3142;

    // Pre-HC devices do not necessarily support multiple display surfaces.
    private static boolean mSupportsMultipleDisplaySurfaces = (VERSION.SDK_INT >= 11);

    /** A Map that tracks which objects are wrapped by EGLSurfaces */
    private static HashMap<Object, EGLSurface> mSurfaceSources = new HashMap<Object, EGLSurface>();

    /** A Map for performing reference counting over shared objects across RenderTargets */
    private static HashMap<Object, Integer> mRefCounts = new HashMap<Object, Integer>();

    /** Stores the FvlRenderTarget that is focused on the current thread. */
    private static ThreadLocal<FvlRenderTarget> mCurrentTarget = new ThreadLocal<FvlRenderTarget>();

    /** The source for the surface used in this target (if any) */
    private Object mSurfaceSource = null;

    /** The cached EGLConfig instance. */
    private static EGLConfig mEglConfig = null;
    private static EGLConfig mShareEglConfig = null;

    /** The display for which the EGLConfig was chosen. We expect only one. */
    private static EGLDisplay mConfiguredDisplay;

    private EGLDisplay mDisplay;
    private EGLContext mContext;
    private EGLSurface mSurface;

    private boolean mOwnsContext;
    private boolean mOwnsSurface;

    private static HashMap<EGLContext, EGLSurface> mDisplaySurfaces
        = new HashMap<EGLContext, EGLSurface>();

    public static FvlRenderTarget newTarget(int width, int height) {
        sLogger.d("newTarget: width=" + width + " height=" + height);
        EGLDisplay eglDisplay = createDefaultDisplay();
        EGLConfig eglConfig = chooseEglConfig(eglDisplay, false);
        EGLContext eglContext = createContext(eglDisplay, eglConfig);
        EGLSurface eglSurface = createSurface(eglDisplay, width, height);
        FvlRenderTarget result = new FvlRenderTarget(eglDisplay, eglContext, eglSurface, true, true);
        result.addReferenceTo(eglSurface);
        return result;
    }

    public static FvlRenderTarget newTarget(Surface surface, EGLContext context) {
        sLogger.d("newTarget: Surface=" + surface);
        //EGLDisplay eglDisplay = createDefaultDisplay();
        EGLConfig eglConfig = chooseShareEglConfig(mConfiguredDisplay, true);
        EGLContext eglContext = createShareContext(mConfiguredDisplay, eglConfig, context);
        //EGLSurface eglSurface = createSurface(eglDisplay, width, height);
        EGLSurface eglSurface = EGL14.EGL_NO_SURFACE;
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        synchronized (mSurfaceSources) {
            eglSurface = mSurfaceSources.get(surface);
            if (eglSurface == null) {
                eglSurface = EGL14.eglCreateWindowSurface(mConfiguredDisplay, eglConfig, surface, surfaceAttribs, 0);
                mSurfaceSources.put(surface, eglSurface);
                sLogger.d("newTarget: create EGLSurface for Surface=" + surface + " EGLSurface=" + eglSurface);
            }
        }
        checkEglError("eglCreateWindowSurface");
        checkSurface(eglSurface);

        FvlRenderTarget result = new FvlRenderTarget(mConfiguredDisplay, eglContext, eglSurface, true, true);
        result.addReferenceTo(eglSurface);
        result.getWidth();
        result.getHeight();
        return result;
    }

    public static void showCurrent() {
        EGLDisplay curDisplay = EGL14.eglGetCurrentDisplay();
        EGLContext curContext = EGL14.eglGetCurrentContext();
        EGLSurface curSurface = EGL14.eglGetCurrentSurface(EGL10.EGL_DRAW);
        sLogger.d("showCurrent: curDisplay=" +
                curDisplay + " curContext=" + curContext + " curSurface=" + curSurface);

    }

    public static FvlRenderTarget currentTarget() {
        sLogger.d("currentTarget");
        // As RenderTargets are immutable, we can safely return the last focused instance on this
        // thread, as we know it cannot have changed, and therefore must be current.
        return mCurrentTarget.get();
    }

    public FvlRenderTarget forSurfaceTexture(SurfaceTexture surfaceTexture) {
        sLogger.d("forSurfaceTexture");
        EGLConfig eglConfig = chooseEglConfig(mDisplay, false);
        EGLSurface eglSurf = null;
        synchronized (mSurfaceSources) {
            eglSurf = mSurfaceSources.get(surfaceTexture);
            if (eglSurf == null) {
                eglSurf = EGL14.eglCreateWindowSurface(mDisplay, eglConfig, surfaceTexture, null, 0);
                mSurfaceSources.put(surfaceTexture, eglSurf);
            }
        }
        checkEglError("eglCreateWindowSurface");
        checkSurface(eglSurf);
        FvlRenderTarget result = new FvlRenderTarget(mDisplay, mContext, eglSurf, false, true);
        result.setSurfaceSource(surfaceTexture);
        result.addReferenceTo(eglSurf);
        return result;
    }

    /**
     * Queries the surface's width.
     */
    public int getWidth() {
        if (mDisplay != EGL14.EGL_NO_DISPLAY && mSurface != EGL14.EGL_NO_SURFACE) {
            int[] value = new int[1];
            EGL14.eglQuerySurface(mDisplay, mSurface, EGL14.EGL_WIDTH, value, 0);
            sLogger.d("getWidth " + value[0]);
            return value[0];
        }
        return -1;
    }

    /**
     * Queries the surface's height.
     */
    public int getHeight() {
        if (mDisplay != EGL14.EGL_NO_DISPLAY && mSurface != EGL14.EGL_NO_SURFACE) {
            int[] value = new int[1];
            EGL14.eglQuerySurface(mDisplay, mSurface, EGL14.EGL_HEIGHT, value, 0);
            sLogger.d("getHeight " + value[0]);
            return value[0];
        }
        return -1;
    }

    public void releaseSurface() {
        sLogger.d("releaseSurface");

        if (mDisplay != null && mDisplay != EGL14.EGL_NO_DISPLAY
                && mSurface != null && mSurface != EGL14.EGL_NO_SURFACE) {
            focusNone();
            sLogger.d("updateSurface: old mSurface=" + mSurface);
            EGL14.eglDestroySurface(mDisplay, mSurface);
        }
        mSurface = EGL14.EGL_NO_SURFACE;
    }

    public FvlRenderTarget updateSurface(Surface surface) {
        sLogger.d("updateSurface: Surface=" + surface);        


        if (mDisplay != null && mDisplay != EGL14.EGL_NO_DISPLAY
                && mSurface != null && mSurface != EGL14.EGL_NO_SURFACE) {
            focusNone();
            sLogger.d("updateSurface: old mSurface=" + mSurface);
            EGL14.eglDestroySurface(mDisplay, mSurface);
        }
        mSurface = EGL14.EGL_NO_SURFACE;
        
        if (surface == null || !surface.isValid()) {
            sLogger.e("createEGLSurface: surface is invalid.");
            return null;
        }
        
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        
        mSurface = EGL14.eglCreateWindowSurface(mDisplay, mEglConfig, surface,
                surfaceAttribs, 0);
        if (mSurface == null || mSurface == EGL14.EGL_NO_SURFACE) {
            int error = EGL14.eglGetError();
            if (error == EGL14.EGL_BAD_NATIVE_WINDOW) {
                sLogger.e("createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
            }
            return null;
        }
        checkEglError("eglCreateWindowSurface");
        
        sLogger.d("updateSurface: new mSurface=" + mSurface);

        focus();
        
        return null;
    }

    public FvlRenderTarget forSurface(Surface surface) {
        sLogger.d("forSurface");
        //int[] surfaceAttribs = {
        //        EGL14.EGL_NONE
        //};
        //EGLDisplay eglDisplay = createDefaultDisplay();
        EGLConfig eglConfig = chooseEglConfig(mDisplay, false);
        //EGLContext eglContext = createContext(eglDisplay, eglConfig);
        EGLSurface eglSurf = null;
        //releaseSurface();        
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        synchronized (mSurfaceSources) {
            eglSurf = mSurfaceSources.get(surface);
            if (eglSurf == null) {
                eglSurf = EGL14.eglCreateWindowSurface(mDisplay, eglConfig, surface, surfaceAttribs, 0);
                mSurfaceSources.put(surface, eglSurf);
                sLogger.d("forSurface: create EGLSurface for Surface=" + surface + " EGLSurface=" + eglSurf);
            }
        }
        checkEglError("eglCreateWindowSurface");
        checkSurface(eglSurf);
        FvlRenderTarget result = new FvlRenderTarget(mDisplay, mContext, eglSurf, false, true);
        result.setSurfaceSource(surface);
        result.addReferenceTo(eglSurf);
        //result.focus();
        result.getWidth();
        result.getHeight();
        return result;
    }

    public FvlRenderTarget forShareSurface(Surface surface) {
        sLogger.d("forShareSurface");
        //int[] surfaceAttribs = {
        //        EGL14.EGL_NONE
        //};
        //EGLDisplay eglDisplay = createDefaultDisplay();
        EGLConfig eglConfig = chooseEglConfig(mDisplay, true);
        EGLContext eglContext = createShareContext(mDisplay, eglConfig, mContext);
        EGLSurface eglSurf = null;
        //releaseSurface();
        synchronized (mSurfaceSources) {
            eglSurf = mSurfaceSources.get(surface);
            if (eglSurf == null) {
                eglSurf = EGL14.eglCreateWindowSurface(mDisplay, eglConfig, surface, null, 0);
                mSurfaceSources.put(surface, eglSurf);
            }
        }
        checkEglError("eglCreateWindowSurface");
        checkSurface(eglSurf);
        FvlRenderTarget result = new FvlRenderTarget(mDisplay, eglContext, eglSurf, true, true);
        result.setSurfaceSource(surface);
        result.addReferenceTo(eglSurf);
        //result.focus();
        return result;
    }

    public void registerAsDisplaySurface() {
        sLogger.d("registerAsDisplaySurface");
        if (!mSupportsMultipleDisplaySurfaces) {
            // Note that while this does in effect change FvlRenderTarget instances (by modifying
            // their returned EGLSurface), breaking the immutability requirement, it does not modify
            // the current target. This is important so that the instance returned in
            // currentTarget() remains accurate.
            EGLSurface currentSurface = mDisplaySurfaces.get(mContext);
            if (currentSurface != null && !currentSurface.equals(mSurface)) {
                throw new RuntimeException("This device supports only a single display surface!");
            } else {
                mDisplaySurfaces.put(mContext, mSurface);
            }
        }
    }

    public void unregisterAsDisplaySurface() {
        sLogger.d("unregisterAsDisplaySurface");
        if (!mSupportsMultipleDisplaySurfaces) {
            mDisplaySurfaces.put(mContext, null);
        }
    }

    public void focus() {
        sLogger.d("focus");
        FvlRenderTarget current = mCurrentTarget.get();
        // We assume RenderTargets are immutable, so that we do not need to focus if the current
        // FvlRenderTarget has not changed.
        //if (current != this) {
            sLogger.d("focus begin : mDisplay=" + mDisplay + " mContext=" + mContext);
            EGL14.eglMakeCurrent(mDisplay, surface(), surface(), mContext);
            mCurrentTarget.set(this);
            sLogger.d("focus end");
        //}
    }

    public static void focusNone() {
        sLogger.d("focusNone begin");
        EGL14.eglMakeCurrent(EGL14.eglGetCurrentDisplay(),
                           EGL14.EGL_NO_SURFACE,
                           EGL14.EGL_NO_SURFACE,
                           EGL14.EGL_NO_CONTEXT);
        mCurrentTarget.set(null);
        checkEglError("eglMakeCurrent");
        sLogger.d("focusNone end");
    }

    public void swapBuffers() {
        sLogger.d("swapBuffers");
        EGL14.eglSwapBuffers(mDisplay, surface());
        checkEglError("swapBuffers");
    }

    public EGLContext getContext() {
        sLogger.d("getContext");
        return mContext;
    }

    public static EGLContext currentContext() {
        sLogger.d("currentContext");
        FvlRenderTarget current = FvlRenderTarget.currentTarget();
        return current != null ? current.getContext() : EGL14.EGL_NO_CONTEXT;
    }

    public void release() {
        sLogger.d("release");
        if (mOwnsContext) {
            EGL14.eglDestroyContext(mDisplay, mContext);
            mContext = EGL14.EGL_NO_CONTEXT;
        }
        if (mOwnsSurface) {
            synchronized (mSurfaceSources) {
                if (removeReferenceTo(mSurface)) {
                    EGL14.eglDestroySurface(mDisplay, mSurface);
                    mSurface = EGL14.EGL_NO_SURFACE;
                    mSurfaceSources.remove(mSurfaceSource);
                }
            }
        }
    }

    @Override
    public String toString() {
        return "FvlRenderTarget(" + mDisplay + ", " + mContext + ", " + mSurface +")";
    }

    private void setSurfaceSource(Object source) {
        sLogger.d("setSurfaceSource");
        mSurfaceSource = source;
    }

    private void addReferenceTo(Object object) {
        sLogger.d("addReferenceTo");
        Integer refCount = mRefCounts.get(object);
        if (refCount != null) {
            mRefCounts.put(object, refCount + 1);
        } else {
            mRefCounts.put(object, 1);
        }
    }

    private boolean removeReferenceTo(Object object) {
        sLogger.d("removeReferenceTo");
        Integer refCount = mRefCounts.get(object);
        if (refCount != null && refCount > 0) {
            --refCount;
            mRefCounts.put(object, refCount);
            return refCount == 0;
        } else {
            Log.e("FvlRenderTarget", "Removing reference of already released: " + object + "!");
            return false;
        }
    }

    private static EGLConfig chooseEglConfig(EGLDisplay display, boolean share) {
        sLogger.d("chooseEglConfig");
        if (mEglConfig == null || !display.equals(mConfiguredDisplay)) {
            int[] numConfigs = new int[1];
            EGLConfig[] configs = new EGLConfig[1];
            int[] attribList = share ? getDesiredShareConfig() : getDesiredConfig();
            if (!EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.length,
                    numConfigs, 0)) {
                throw new IllegalArgumentException("EGL Error: eglChooseConfig failed " +
                        getEGLErrorString(EGL14.eglGetError()));
            } else if (numConfigs[0] > 0) {
                mEglConfig = configs[0];
                mConfiguredDisplay = display;
                sLogger.d("chooseEglConfig:  mEglConfig=" + mEglConfig);
            }
        }
        return mEglConfig;
    }

    private static EGLConfig chooseShareEglConfig(EGLDisplay display, boolean share) {
        sLogger.d("chooseEglConfig");
        if (mShareEglConfig == null || !display.equals(mConfiguredDisplay)) {
            int[] numConfigs = new int[1];
            EGLConfig[] configs = new EGLConfig[1];
            int[] attribList = share ? getDesiredShareConfig() : getDesiredConfig();
            if (!EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.length,
                    numConfigs, 0)) {
                throw new IllegalArgumentException("EGL Error: eglChooseConfig failed " +
                        getEGLErrorString(EGL14.eglGetError()));
            } else if (numConfigs[0] > 0) {
                mShareEglConfig = configs[0];
                mConfiguredDisplay = display;
                sLogger.d("chooseEglConfig:  mShareEglConfig=" + mShareEglConfig);
            }
        }
        return mShareEglConfig;
    }

    private static int[] getDesiredConfig() {
        sLogger.d("getDesiredConfig");
        return  new int[] {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL14.EGL_NONE
        };
    }

    private static int[] getDesiredShareConfig() {
        sLogger.d("getDesiredShareConfig");
        return new int[] {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL_RECORDABLE_ANDROID, 1,
                EGL14.EGL_NONE
        };
    }

    private FvlRenderTarget(EGLDisplay display, EGLContext context, EGLSurface surface,
            boolean ownsContext, boolean ownsSurface) {
        sLogger.d("FvlRenderTarget create");
        mDisplay = display;
        mContext = context;
        mSurface = surface;
        mOwnsContext = ownsContext;
        mOwnsSurface = ownsSurface;
    }

    private EGLSurface surface() {
        sLogger.d("surface");
        if (mSupportsMultipleDisplaySurfaces) {
            sLogger.d("surface mSupportsMultipleDisplaySurfaces: EGLSurface: " + mSurface);
            return mSurface;
        } else {
            EGLSurface displaySurface = mDisplaySurfaces.get(mContext);
            return displaySurface != null ? displaySurface : mSurface;
        }
    }

    private static void initEgl(EGLDisplay display) {
        sLogger.d("initEgl");
        int[] version = new int[2];
        if (!EGL14.eglInitialize(display, version, 0, version, 1)) {
            throw new RuntimeException("EGL Error: eglInitialize failed " +
                    getEGLErrorString(EGL14.eglGetError()));
        }
    }

    private static EGLDisplay createDefaultDisplay() {
        sLogger.d("createDefaultDisplay");
        EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        checkDisplay(display);
        initEgl(display);
        return display;
    }

    private static EGLContext createContext(EGLDisplay display, EGLConfig config) {
        sLogger.d("createContext");
        // Configure context for OpenGL ES 2.0.
        int[] attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        EGLContext ctxt = EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, attrib_list, 0);
        checkContext(ctxt);
        return ctxt;
    }

    private static EGLContext createShareContext(EGLDisplay display, EGLConfig config, EGLContext context) {
        sLogger.d("createShareContext");
        // Configure context for OpenGL ES 2.0.
        int[] attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        EGLContext ctxt = EGL14.eglCreateContext(display, config, context, attrib_list, 0);
        checkContext(ctxt);
        return ctxt;
    }

    private static EGLSurface createSurface(EGLDisplay display, int width, int height) {
        sLogger.d("createSurface: width=" + width + " height=" + height);
        EGLConfig eglConfig = chooseEglConfig(display, false);
        int[] attribs = { EGL14.EGL_WIDTH, width, EGL14.EGL_HEIGHT, height, EGL14.EGL_NONE };
        return EGL14.eglCreatePbufferSurface(display, eglConfig, attribs, 0);
    }

    private static void checkDisplay(EGLDisplay display) {
        sLogger.d("checkDisplay");
        if (display == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("EGL Error: Bad display: "
                    + getEGLErrorString(EGL14.eglGetError()));
        }
    }

    private static void checkContext(EGLContext context) {
        sLogger.d("checkContext");
        if (context == EGL14.EGL_NO_CONTEXT) {
            throw new RuntimeException("EGL Error: Bad context: "
                    + getEGLErrorString(EGL14.eglGetError()));
        }
    }

    private static void checkSurface(EGLSurface surface) {
        sLogger.d("checkSurface");
        if (surface == EGL14.EGL_NO_SURFACE) {
            throw new RuntimeException("EGL Error: Bad surface: "
                    + getEGLErrorString(EGL14.eglGetError()));
        }
    }

    private static void checkEglError(String command) {
        int error = EGL14.eglGetError();
        if (error != EGL14.EGL_SUCCESS) {
            throw new RuntimeException("Error executing " + command + "! EGL error = 0x"
                + Integer.toHexString(error));
        }
    }

    private static String getEGLErrorString(int eglError) {
        return "EGL Error: 0x" + Integer.toHexString(eglError);
    }

}

